Skip to content

Feat/wasm poc#25

Open
rzofchak-a2ai wants to merge 3 commits intomainfrom
feat/wasm-poc
Open

Feat/wasm poc#25
rzofchak-a2ai wants to merge 3 commits intomainfrom
feat/wasm-poc

Conversation

@rzofchak-a2ai
Copy link
Copy Markdown

@rzofchak-a2ai rzofchak-a2ai commented Apr 16, 2026

added wasm bindings to spackle commands to recreate spackle::Project::generate() done in typescript in the aggregate (insert moneyball gif here). now includes actual hook execution via Bun.spawn as well

WASM.md contains a claude-generated rundown of all the changes. but generally:

  • wasm support added with wasm-bindgen (10 exports, all JSON-in/JSON-out)
  • in-memory renditions of functions to support wasm exports for host-driven i/o
  • poc/ restructured into src/wasm/ (pure compute, no I/O) + src/host/ (fs + Bun.spawn) + src/spackle.ts (orchestration). each file headered with which side of the line it's on
  • automated bun test suite — cd poc && bun test runs 36 tests across wasm contract, host helpers, and e2e scenarios (generate + hook exec against real fixtures)
  • orchestration matches native Project::generate semantics by default: errors if outDir exists (opt-in overwrite: true), fail-fast on first template render error (opt-in allowTemplateErrors: true), copy-then-render order so templates win on path collisions
  • release pipeline: tagged releases attach a tarball with all 3 wasm-pack targets (web/nodejs/bundler) as a github release asset, installable via bun add <release-url>. ci builds all 3 targets + runtime-import smoke-tests the nodejs target so broken bindings surface on every pr
  • table-driven tests on the rust side (44 with --features wasm, 39 without)

run just poc to get poc output:

cd poc && bun run scripts/demo.ts
Loading WASM module...
WASM module loaded.

=== check(proj2) ===
  valid=true
  name=(unnamed) slots=1 hooks=0

=== check(bad_default_slot_val) ===
  valid=false errors=["type mismatch for key default_number: expected a number"]

=== generate(proj2) → output/proj2 ===
  wrote=1 copied=1 hooks_planned=0
  good.j2 → good  hello world

=== generate(hook, runHooks=true) → output/hook ===
  RAN hook_1 exit=0 stdout="This is logged to stdout" stderr="This is logged to stderr"
  RAN hook_2 exit=0 stdout="This is logged to stdout" stderr="This is logged to stderr"
  RAN hook_3 exit=0 stdout="" stderr=""

=== evaluate_hooks(hook_ran_cond) ===
  hook_1: WOULD RUN  cmd=["true"]
  hook_2: WOULD RUN  cmd=["true"]
  dep_hook_should_run: WOULD RUN  cmd=["true"]
  dep_hook_should_not_run: WOULD RUN  cmd=["true"]

Done.

run just test-poc to build wasm + run the bun test suite:

cd poc && bun install && bun test
bun install v1.2.8 (adab0f64)

Checked 5 installs across 6 packages (no changes) [19.00ms]
bun test v1.2.8 (adab0f64)

tests/e2e.test.ts:
✓ check > proj2: valid, returns config [6.63ms]
✓ check > bad_default_slot_val: invalid with slot-type error [0.59ms]
✓ generate: proj2 (clean happy path) > renders good.j2 and copies subdir/file.txt [5.21ms]
✓ generate: universal (filename-templated non-template) > renders {{_output_name}}.j2 and {{_project_name}}.j2 to use computed specials [4.67ms]
✓ generate: hook fixture with runHooks > spawns each should_run hook and captures stdout/stderr [13.97ms]
✓ generate: invalid slot data fails fast > throws before touching disk when validateSlotData fails [0.93ms]
✓ generate: outDir-exists protection (native parity) > throws when outDir already exists (default: overwrite=false) [0.74ms]
✓ generate: outDir-exists protection (native parity) > proceeds when overwrite: true [1.62ms]
✓ generate: template-error fail-fast (native parity) > throws on first error, references only that entry, attaches full batch [1.67ms]
✓ generate: template-error fail-fast (native parity) > does not touch disk when failing fast [3.39ms]
✓ generate: template-error fail-fast (native parity) > allowTemplateErrors: true writes only the successful entries [2.15ms]
✓ generate: copy→render precedence (native parity) > a template at path 'x.j2' overwrites a plain file at 'x' [1.70ms]

tests/host.test.ts:
✓ readSpackleConfig > reads spackle.toml content verbatim [0.45ms]
✓ walkTemplates > collects .j2 files with relative paths; skips non-j2 and ignore [0.71ms]
✓ writeRenderedFiles > writes each rendered file; skips entries with an error; creates parent dirs [0.39ms]
✓ copyNonTemplates > copies non-j2 files and templates the destination filename [1.34ms]
✓ executeHookPlan > runs should_run=true entries, yields skipped for the rest [2.37ms]
✓ executeHookPlan > captures stderr separately from stdout [2.05ms]

tests/wasm.test.ts:
✓ parseConfig > parses proj1 toml into structured config [0.24ms]
✓ parseConfig > throws for invalid toml [0.04ms]
✓ validateConfig > valid proj1 [0.18ms]
✓ validateConfig > duplicate keys returns valid=false with errors array [0.10ms]
✓ checkProject > proj2 is fully valid [0.29ms]
✓ checkProject > always returns {valid, errors} shape even on malformed input [0.07ms]
✓ validateSlotData > accepts valid slot_1/slot_2/slot_3 values [0.05ms]
✓ validateSlotData > rejects wrong type for Number slot [0.04ms]
✓ renderTemplates > renders proj2 template with filename untouched [0.13ms]
✓ renderTemplates > surfaces per-file errors for undefined vars without failing the batch [0.53ms]
✓ evaluateHooks > plans all hooks for the 'hook' fixture [0.40ms]
✓ evaluateHooks > conditional hooks honor the hook_ran_<key> injection [0.37ms]
✓ evaluateHooks > template errors mark should_run=false with skip_reason [0.12ms]
✓ renderString (one-off template) > substitutes variables in a path-like string [0.05ms]
✓ renderString (one-off template) > throws on undefined variable [0.05ms]
✓ getOutputName + getProjectName > getOutputName returns the last path segment
✓ getOutputName + getProjectName > getProjectName: config.name wins
✓ getOutputName + getProjectName > getProjectName: falls back to project_dir file_stem [0.07ms]

 36 pass
 0 fail
 95 expect() calls
Ran 36 tests across 3 files. [137.00ms]

@rzofchak-a2ai rzofchak-a2ai requested a review from andriygm April 16, 2026 19:21
@rzofchak-a2ai rzofchak-a2ai marked this pull request as ready for review April 17, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant